1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.solr.analytics.statistics;
19  
20  import java.lang.invoke.MethodHandles;
21  import java.text.ParseException;
22  import java.util.ArrayList;
23  import java.util.HashMap;
24  import java.util.HashSet;
25  import java.util.LinkedHashMap;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Set;
29  import java.util.TreeMap;
30  
31  import org.apache.lucene.queries.function.ValueSource;
32  import org.apache.lucene.queries.function.valuesource.BytesRefFieldSource;
33  import org.apache.lucene.queries.function.valuesource.DoubleFieldSource;
34  import org.apache.lucene.queries.function.valuesource.FloatFieldSource;
35  import org.apache.lucene.queries.function.valuesource.IntFieldSource;
36  import org.apache.lucene.queries.function.valuesource.LongFieldSource;
37  import org.apache.solr.analytics.expression.ExpressionFactory;
38  import org.apache.solr.analytics.request.ExpressionRequest;
39  import org.apache.solr.analytics.util.AnalyticsParams;
40  import org.apache.solr.analytics.util.AnalyticsParsers;
41  import org.apache.solr.analytics.util.valuesource.AbsoluteValueDoubleFunction;
42  import org.apache.solr.analytics.util.valuesource.AddDoubleFunction;
43  import org.apache.solr.analytics.util.valuesource.ConcatStringFunction;
44  import org.apache.solr.analytics.util.valuesource.ConstDateSource;
45  import org.apache.solr.analytics.util.valuesource.ConstDoubleSource;
46  import org.apache.solr.analytics.util.valuesource.ConstStringSource;
47  import org.apache.solr.analytics.util.valuesource.DateFieldSource;
48  import org.apache.solr.analytics.util.valuesource.DateMathFunction;
49  import org.apache.solr.analytics.util.valuesource.DivDoubleFunction;
50  import org.apache.solr.analytics.util.valuesource.DualDoubleFunction;
51  import org.apache.solr.analytics.util.valuesource.FilterFieldSource;
52  import org.apache.solr.analytics.util.valuesource.LogDoubleFunction;
53  import org.apache.solr.analytics.util.valuesource.MultiDateFunction;
54  import org.apache.solr.analytics.util.valuesource.MultiDoubleFunction;
55  import org.apache.solr.analytics.util.valuesource.MultiplyDoubleFunction;
56  import org.apache.solr.analytics.util.valuesource.NegateDoubleFunction;
57  import org.apache.solr.analytics.util.valuesource.PowDoubleFunction;
58  import org.apache.solr.analytics.util.valuesource.ReverseStringFunction;
59  import org.apache.solr.analytics.util.valuesource.SingleDoubleFunction;
60  import org.apache.solr.common.SolrException;
61  import org.apache.solr.common.SolrException.ErrorCode;
62  import org.apache.solr.schema.FieldType;
63  import org.apache.solr.schema.IndexSchema;
64  import org.apache.solr.schema.SchemaField;
65  import org.apache.solr.schema.StrField;
66  import org.apache.solr.schema.TrieDateField;
67  import org.apache.solr.schema.TrieDoubleField;
68  import org.apache.solr.schema.TrieFloatField;
69  import org.apache.solr.schema.TrieIntField;
70  import org.apache.solr.schema.TrieLongField;
71  import org.apache.solr.util.DateFormatUtil;
72  import org.slf4j.Logger;
73  import org.slf4j.LoggerFactory;
74  
75  import com.google.common.base.Supplier;
76  
77  public class StatsCollectorSupplierFactory {
78    private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
79    
80    // FunctionTypes
81    final static int NUMBER_TYPE = 0;
82    final static int DATE_TYPE = 1;
83    final static int STRING_TYPE = 2;
84    final static int FIELD_TYPE = 3;
85    final static int FILTER_TYPE = 4;
86    
87    /**
88     * Builds a Supplier that will generate identical arrays of new StatsCollectors.
89     * 
90     * @param schema The Schema being used.
91     * @param exRequests The expression requests to generate a StatsCollector[] from.
92     * @return A Supplier that will return an array of new StatsCollector.
93     */
94    @SuppressWarnings("unchecked")
95    public static Supplier<StatsCollector[]> create(IndexSchema schema, List<ExpressionRequest> exRequests ) {
96      final Map<String, Set<String>> collectorStats =  new TreeMap<>();
97      final Map<String, Set<Integer>> collectorPercs =  new TreeMap<>();
98      final Map<String, ValueSource> collectorSources =  new TreeMap<>();
99      
100     // Iterate through all expression request to make a list of ValueSource strings
101     // and statistics that need to be calculated on those ValueSources.
102     for (ExpressionRequest expRequest : exRequests) {
103       String statExpression = expRequest.getExpressionString();
104       Set<String> statistics = getStatistics(statExpression);
105       if (statistics == null) {
106         continue;
107       }
108       for (String statExp : statistics) {
109         String stat;
110         String operands;
111         try {
112           stat = statExp.substring(0, statExp.indexOf('(')).trim();
113           operands = statExp.substring(statExp.indexOf('(')+1, statExp.lastIndexOf(')')).trim();
114         } catch (Exception e) {
115           throw new SolrException(ErrorCode.BAD_REQUEST,"Unable to parse statistic: ["+statExpression+"]",e);
116         }
117         String[] arguments = ExpressionFactory.getArguments(operands);
118         String source = arguments[0];
119         if (stat.equals(AnalyticsParams.STAT_PERCENTILE)) {
120           // The statistic is a percentile, extra parsing is required
121           if (arguments.length<2) {
122             throw new SolrException(ErrorCode.BAD_REQUEST,"Too few arguments given for "+stat+"() in ["+statExp+"].");
123           } else if (arguments.length>2) {
124             throw new SolrException(ErrorCode.BAD_REQUEST,"Too many arguments given for "+stat+"() in ["+statExp+"].");
125           }
126           source = arguments[1];
127           Set<Integer> percs = collectorPercs.get(source);
128           if (percs == null) {
129             percs = new HashSet<>();
130             collectorPercs.put(source, percs);
131           }
132           try {
133             int perc = Integer.parseInt(arguments[0]);
134             if (perc>0 && perc<100) {
135               percs.add(perc);
136             } else {
137               throw new SolrException(ErrorCode.BAD_REQUEST,"The percentile in ["+statExp+"] is not between 0 and 100, exculsive.");
138             }
139           } catch (NumberFormatException e) {
140             throw new SolrException(ErrorCode.BAD_REQUEST,"\""+arguments[0]+"\" cannot be converted into a percentile.",e);
141           }
142         } else if (arguments.length>1) {
143           throw new SolrException(ErrorCode.BAD_REQUEST,"Too many arguments given for "+stat+"() in ["+statExp+"].");
144         } else if (arguments.length==0) {
145           throw new SolrException(ErrorCode.BAD_REQUEST,"No arguments given for "+stat+"() in ["+statExp+"].");
146         } 
147         // Only unique ValueSources will be made; therefore statistics must be accumulated for
148         // each ValueSource, even across different expression requests
149         Set<String> stats = collectorStats.get(source);
150         if (stats == null) {
151           stats = new HashSet<>();
152           collectorStats.put(source, stats);
153         }
154         if(AnalyticsParams.STAT_PERCENTILE.equals(stat)) {
155           stats.add(stat + "_"+ arguments[0]);
156         } else {
157           stats.add(stat);
158         }
159       }
160     }
161     String[] keys = collectorStats.keySet().toArray(new String[0]);
162     for (String sourceStr : keys) {
163       // Build one ValueSource for each unique value source string
164       ValueSource source = buildSourceTree(schema, sourceStr);
165       if (source == null) {
166         throw new SolrException(ErrorCode.BAD_REQUEST,"The statistic ["+sourceStr+"] could not be parsed.");
167       }
168       String builtString = source.toString();
169       collectorSources.put(builtString,source);
170       // Replace the user given string with the correctly built string
171       if (!builtString.equals(sourceStr)) {
172         Set<String> stats = collectorStats.remove(sourceStr);
173         if (stats!=null) {
174           collectorStats.put(builtString, stats);
175         }
176         Set<Integer> percs = collectorPercs.remove(sourceStr);
177         if (percs!=null) {
178           collectorPercs.put(builtString, percs);
179         }
180         for (ExpressionRequest er : exRequests) {
181           er.setExpressionString(er.getExpressionString().replace(sourceStr, builtString));
182         }
183       }
184     }
185     if (collectorSources.size()==0) {
186       return new Supplier<StatsCollector[]>() {
187         @Override
188         public StatsCollector[] get() {
189           return new StatsCollector[0];
190         }
191       };
192     }
193     
194     log.info("Stats objects: "+collectorStats.size()+" sr="+collectorSources.size()+" pr="+collectorPercs.size() );
195     
196     // All information is stored in final arrays so that nothing 
197     // has to be computed when the Supplier's get() method is called.
198     final Set<String>[] statsArr = collectorStats.values().toArray(new Set[0]);
199     final ValueSource[] sourceArr = collectorSources.values().toArray(new ValueSource[0]);
200     final boolean[] uniqueBools = new boolean[statsArr.length];
201     final boolean[] medianBools = new boolean[statsArr.length];
202     final boolean[] numericBools = new boolean[statsArr.length];
203     final boolean[] dateBools = new boolean[statsArr.length];
204     final double[][] percsArr = new double[statsArr.length][];
205     final String[][] percsNames = new String[statsArr.length][];
206     for (int count = 0; count < sourceArr.length; count++) {
207       uniqueBools[count] = statsArr[count].contains(AnalyticsParams.STAT_UNIQUE);
208       medianBools[count] = statsArr[count].contains(AnalyticsParams.STAT_MEDIAN);
209       numericBools[count] = statsArr[count].contains(AnalyticsParams.STAT_SUM)||statsArr[count].contains(AnalyticsParams.STAT_SUM_OF_SQUARES)||statsArr[count].contains(AnalyticsParams.STAT_MEAN)||statsArr[count].contains(AnalyticsParams.STAT_STANDARD_DEVIATION);
210       dateBools[count] = (sourceArr[count] instanceof DateFieldSource) | (sourceArr[count] instanceof MultiDateFunction) | (sourceArr[count] instanceof ConstDateSource);
211       Set<Integer> ps = collectorPercs.get(sourceArr[count].toString());
212       if (ps!=null) {
213         percsArr[count] = new double[ps.size()];
214         percsNames[count] = new String[ps.size()];
215         int percCount = 0;
216         for (int p : ps) {
217           percsArr[count][percCount] = p/100.0;
218           percsNames[count][percCount++] = AnalyticsParams.STAT_PERCENTILE+"_"+p;
219         }
220       }
221     }
222     // Making the Supplier
223     return new Supplier<StatsCollector[]>() {
224       public StatsCollector[] get() {
225         StatsCollector[] collectors = new StatsCollector[statsArr.length];
226         for (int count = 0; count < statsArr.length; count++) {
227           if(numericBools[count]){
228             StatsCollector sc = new NumericStatsCollector(sourceArr[count], statsArr[count]);
229             if(uniqueBools[count]) sc = new UniqueStatsCollector(sc);
230             if(medianBools[count]) sc = new MedianStatsCollector(sc);
231             if(percsArr[count]!=null) sc = new PercentileStatsCollector(sc,percsArr[count],percsNames[count]);
232             collectors[count]=sc;
233           } else if (dateBools[count]) {
234             StatsCollector sc = new MinMaxStatsCollector(sourceArr[count], statsArr[count]);
235             if(uniqueBools[count]) sc = new UniqueStatsCollector(sc);
236             if(medianBools[count]) sc = new DateMedianStatsCollector(sc);
237             if(percsArr[count]!=null) sc = new PercentileStatsCollector(sc,percsArr[count],percsNames[count]);
238            collectors[count]=sc;
239           } else {
240             StatsCollector sc = new MinMaxStatsCollector(sourceArr[count], statsArr[count]);
241             if(uniqueBools[count]) sc = new UniqueStatsCollector(sc);
242             if(medianBools[count]) sc = new MedianStatsCollector(sc);
243             if(percsArr[count]!=null) sc = new PercentileStatsCollector(sc,percsArr[count],percsNames[count]);
244             collectors[count]=sc;
245           }
246         }
247         return collectors;
248       }
249     };
250   }
251   
252   /**
253    * Finds the set of statistics that must be computed for the expression.
254    * @param expression The string representation of an expression
255    * @return The set of statistics (sum, mean, median, etc.) found in the expression
256    */
257   public static Set<String> getStatistics(String expression) {
258     HashSet<String> set = new HashSet<>();
259     int firstParen = expression.indexOf('(');
260     if (firstParen>0) {
261       String topOperation = expression.substring(0,firstParen).trim();
262       if (AnalyticsParams.ALL_STAT_SET.contains(topOperation)) {
263         set.add(expression);
264       } else if (!(topOperation.equals(AnalyticsParams.CONSTANT_NUMBER)||topOperation.equals(AnalyticsParams.CONSTANT_DATE)||topOperation.equals(AnalyticsParams.CONSTANT_STRING))) {
265         String operands = expression.substring(firstParen+1, expression.lastIndexOf(')')).trim();
266         String[] arguments = ExpressionFactory.getArguments(operands);
267         for (String argument : arguments) {
268           Set<String> more = getStatistics(argument);
269           if (more!=null) {
270             set.addAll(more);
271           }
272         }
273       }
274     }
275     if (set.size()==0) {
276       return null;
277     }
278     return set;
279   }
280   
281   /**
282    * Builds a Value Source from a given string
283    * 
284    * @param schema The schema being used.
285    * @param expression The string to be turned into an expression.
286    * @return The completed ValueSource
287    */
288   private static ValueSource buildSourceTree(IndexSchema schema, String expression) {
289     return buildSourceTree(schema,expression,FIELD_TYPE);
290   }
291   
292   /**
293    * Builds a Value Source from a given string and a given source type
294    * 
295    * @param schema The schema being used.
296    * @param expression The string to be turned into an expression.
297    * @param sourceType The type of source that must be returned.
298    * @return The completed ValueSource
299    */
300   private static ValueSource buildSourceTree(IndexSchema schema, String expression, int sourceType) {
301     int expressionType = getSourceType(expression);
302     if (sourceType != FIELD_TYPE && expressionType != FIELD_TYPE && 
303         expressionType != FILTER_TYPE && expressionType != sourceType) {
304       return null;
305     }
306     switch (expressionType) {
307     case NUMBER_TYPE : return buildNumericSource(schema, expression);
308     case DATE_TYPE : return buildDateSource(schema, expression);
309     case STRING_TYPE : return buildStringSource(schema, expression);
310     case FIELD_TYPE : return buildFieldSource(schema, expression, sourceType);
311     case FILTER_TYPE : return buildFilterSource(schema, expression.substring(expression.indexOf('(')+1,expression.lastIndexOf(')')), sourceType);
312     default : throw new SolrException(ErrorCode.BAD_REQUEST,expression+" is not a valid operation.");
313     }
314   }
315 
316   /**
317    * Determines what type of value source the expression represents.
318    * 
319    * @param expression The expression representing the desired ValueSource
320    * @return NUMBER_TYPE, DATE_TYPE, STRING_TYPE or -1
321    */
322   private static int getSourceType(String expression) {
323     int paren = expression.indexOf('(');
324     if (paren<0) {
325       return FIELD_TYPE;
326     }
327     String operation = expression.substring(0,paren).trim();
328 
329     if (AnalyticsParams.NUMERIC_OPERATION_SET.contains(operation)) {
330       return NUMBER_TYPE;
331     } else if (AnalyticsParams.DATE_OPERATION_SET.contains(operation)) {
332       return DATE_TYPE;
333     } else if (AnalyticsParams.STRING_OPERATION_SET.contains(operation)) {
334       return STRING_TYPE;
335     } else if (operation.equals(AnalyticsParams.FILTER)) {
336       return FILTER_TYPE;
337     }
338     throw new SolrException(ErrorCode.BAD_REQUEST,"The operation \""+operation+"\" in ["+expression+"] is not supported.");
339   }
340   
341   /**
342    *  Builds a value source for a given field, making sure that the field fits a given source type.
343    * @param schema the schema
344    * @param expressionString The name of the field to build a Field Source from.
345    * @param sourceType FIELD_TYPE for any type of field, NUMBER_TYPE for numeric fields, 
346    * DATE_TYPE for date fields and STRING_TYPE for string fields.
347    * @return a value source
348    */
349   private static ValueSource buildFieldSource(IndexSchema schema, String expressionString, int sourceType) {
350     SchemaField sf;
351     try {
352       sf = schema.getField(expressionString);
353     } catch (SolrException e) {
354       throw new SolrException(ErrorCode.BAD_REQUEST,"The field "+expressionString+" does not exist.",e);
355     }
356     FieldType type = sf.getType();
357     if ( type instanceof TrieIntField) {
358       if (sourceType!=NUMBER_TYPE&&sourceType!=FIELD_TYPE) {
359         return null;
360       }
361       return new IntFieldSource(expressionString) {
362         public String description() {
363           return field;
364         }
365       };
366     } else if (type instanceof TrieLongField) {
367       if (sourceType!=NUMBER_TYPE&&sourceType!=FIELD_TYPE) {
368         return null;
369       }
370       return new LongFieldSource(expressionString) {
371         public String description() {
372           return field;
373         }
374       };
375     } else if (type instanceof TrieFloatField) {
376       if (sourceType!=NUMBER_TYPE&&sourceType!=FIELD_TYPE) {
377         return null;
378       }
379       return new FloatFieldSource(expressionString) {
380         public String description() {
381           return field;
382         }
383       };
384     } else if (type instanceof TrieDoubleField) {
385       if (sourceType!=NUMBER_TYPE&&sourceType!=FIELD_TYPE) {
386         return null;
387       }
388       return new DoubleFieldSource(expressionString) {
389         public String description() {
390           return field;
391         }
392       };
393     } else if (type instanceof TrieDateField) {
394       if (sourceType!=DATE_TYPE&&sourceType!=FIELD_TYPE) {
395         return null;
396       }
397       return new DateFieldSource(expressionString) {
398         public String description() {
399           return field;
400         }
401       };
402     } else if (type instanceof StrField) {
403       if (sourceType!=STRING_TYPE&&sourceType!=FIELD_TYPE) {
404         return null;
405       }
406       return new BytesRefFieldSource(expressionString) {
407         public String description() {
408           return field;
409         }
410       };
411     }
412     throw new SolrException(ErrorCode.BAD_REQUEST, type.toString()+" is not a supported field type in Solr Analytics.");
413   }
414   
415   /**
416    * Builds a default is missing source that wraps a given source. A missing value is required for all 
417    * non-field value sources.
418    * @param schema the schema
419    * @param expressionString The name of the field to build a Field Source from.
420    * @param sourceType FIELD_TYPE for any type of field, NUMBER_TYPE for numeric fields, 
421    * DATE_TYPE for date fields and STRING_TYPE for string fields.
422    * @return a value source
423    */
424   @SuppressWarnings("deprecation")
425   private static ValueSource buildFilterSource(IndexSchema schema, String expressionString, int sourceType) {
426     String[] arguments = ExpressionFactory.getArguments(expressionString);
427     if (arguments.length!=2) {
428       throw new SolrException(ErrorCode.BAD_REQUEST,"Invalid arguments were given for \""+AnalyticsParams.FILTER+"\".");
429     }
430     ValueSource delegateSource = buildSourceTree(schema, arguments[0], sourceType);
431     if (delegateSource==null) {
432       return null;
433     }
434     Object defaultObject;
435 
436     ValueSource src = delegateSource;
437     if (delegateSource instanceof FilterFieldSource) {
438       src = ((FilterFieldSource)delegateSource).getRootSource();
439     }
440     if ( src instanceof IntFieldSource) {
441       try {
442         defaultObject = new Integer(arguments[1]);
443       } catch (NumberFormatException e) {
444         throw new SolrException(ErrorCode.BAD_REQUEST,"The filter value "+arguments[1]+" cannot be converted into an integer.",e);
445       }
446     } else if ( src instanceof DateFieldSource || src instanceof MultiDateFunction) {
447       try {
448         defaultObject = DateFormatUtil.parseDate(arguments[1]);
449       } catch (ParseException e) {
450         throw new SolrException(ErrorCode.BAD_REQUEST,"The filter value "+arguments[1]+" cannot be converted into a date.",e);
451       }
452     } else if ( src instanceof LongFieldSource ) {
453       try {
454         defaultObject = new Long(arguments[1]);
455       } catch (NumberFormatException e) {
456         throw new SolrException(ErrorCode.BAD_REQUEST,"The filter value "+arguments[1]+" cannot be converted into a long.",e);
457       }
458     } else if ( src instanceof FloatFieldSource ) {
459       try {
460         defaultObject = new Float(arguments[1]);
461       } catch (NumberFormatException e) {
462         throw new SolrException(ErrorCode.BAD_REQUEST,"The filter value "+arguments[1]+" cannot be converted into a float.",e);
463       }
464     } else if ( src instanceof DoubleFieldSource || src instanceof SingleDoubleFunction ||
465                 src instanceof DualDoubleFunction|| src instanceof MultiDoubleFunction) {
466       try {
467         defaultObject = new Double(arguments[1]);
468       } catch (NumberFormatException e) {
469         throw new SolrException(ErrorCode.BAD_REQUEST,"The filter value "+arguments[1]+" cannot be converted into a double.",e);
470       }
471     } else {
472       defaultObject = arguments[1];
473     }
474     return new FilterFieldSource(delegateSource,defaultObject);
475   } 
476   
477   /**
478    * Recursively parses and breaks down the expression string to build a numeric ValueSource.
479    * 
480    * @param schema The schema to pull fields from.
481    * @param expressionString The expression string to build a ValueSource from.
482    * @return The value source represented by the given expressionString
483    */
484   private static ValueSource buildNumericSource(IndexSchema schema, String expressionString) {
485     int paren = expressionString.indexOf('(');
486     String[] arguments;
487     String operands;
488     if (paren<0) {
489       return buildFieldSource(schema,expressionString,NUMBER_TYPE);
490     } else {
491       try {
492         operands = expressionString.substring(paren+1, expressionString.lastIndexOf(')')).trim();
493       } catch (Exception e) {
494         throw new SolrException(ErrorCode.BAD_REQUEST,"Missing closing parenthesis in ["+expressionString+"]");
495       }
496       arguments = ExpressionFactory.getArguments(operands);
497     }
498     String operation = expressionString.substring(0, paren).trim();
499     if (operation.equals(AnalyticsParams.CONSTANT_NUMBER)) {
500       if (arguments.length!=1) {
501         throw new SolrException(ErrorCode.BAD_REQUEST,"The constant number declaration ["+expressionString+"] does not have exactly 1 argument.");
502       }
503       return new ConstDoubleSource(Double.parseDouble(arguments[0]));
504     } else if (operation.equals(AnalyticsParams.NEGATE)) {
505       if (arguments.length!=1) {
506         throw new SolrException(ErrorCode.BAD_REQUEST,"The negate operation ["+expressionString+"] does not have exactly 1 argument.");
507       }
508       ValueSource argSource = buildNumericSource(schema, arguments[0]);
509       if (argSource==null) {
510         throw new SolrException(ErrorCode.BAD_REQUEST,"The operation \""+AnalyticsParams.NEGATE+"\" requires a numeric field or operation as argument. \""+arguments[0]+"\" is not a numeric field or operation.");
511       }
512       return new NegateDoubleFunction(argSource);
513     }  else if (operation.equals(AnalyticsParams.ABSOLUTE_VALUE)) {
514       if (arguments.length!=1) {
515         throw new SolrException(ErrorCode.BAD_REQUEST,"The absolute value operation ["+expressionString+"] does not have exactly 1 argument.");
516       }
517       ValueSource argSource = buildNumericSource(schema, arguments[0]);
518       if (argSource==null) {
519         throw new SolrException(ErrorCode.BAD_REQUEST,"The operation \""+AnalyticsParams.NEGATE+"\" requires a numeric field or operation as argument. \""+arguments[0]+"\" is not a numeric field or operation.");
520       }
521       return new AbsoluteValueDoubleFunction(argSource);
522     } else if (operation.equals(AnalyticsParams.FILTER)) {
523       return buildFilterSource(schema, operands, NUMBER_TYPE);
524     }
525     List<ValueSource> subExpressions = new ArrayList<>();
526     for (String argument : arguments) {
527       ValueSource argSource = buildNumericSource(schema, argument);
528       if (argSource == null) {
529         throw new SolrException(ErrorCode.BAD_REQUEST,"The operation \""+operation+"\" requires numeric fields or operations as arguments. \""+argument+"\" is not a numeric field or operation.");
530       }
531       subExpressions.add(argSource);
532     }
533     if (operation.equals(AnalyticsParams.ADD)) {
534       return new AddDoubleFunction(subExpressions.toArray(new ValueSource[0]));
535     } else if (operation.equals(AnalyticsParams.MULTIPLY)) {
536       return new MultiplyDoubleFunction(subExpressions.toArray(new ValueSource[0]));
537     } else if (operation.equals(AnalyticsParams.DIVIDE)) {
538       if (subExpressions.size()!=2) {
539         throw new SolrException(ErrorCode.BAD_REQUEST,"The divide operation ["+expressionString+"] does not have exactly 2 arguments.");
540       }
541       return new DivDoubleFunction(subExpressions.get(0),subExpressions.get(1));
542     } else if (operation.equals(AnalyticsParams.POWER)) {
543       if (subExpressions.size()!=2) {
544         throw new SolrException(ErrorCode.BAD_REQUEST,"The power operation ["+expressionString+"] does not have exactly 2 arguments.");
545       }
546       return new PowDoubleFunction(subExpressions.get(0),subExpressions.get(1));
547     } else if (operation.equals(AnalyticsParams.LOG)) {
548       if (subExpressions.size()!=2) {
549         throw new SolrException(ErrorCode.BAD_REQUEST,"The log operation ["+expressionString+"] does not have exactly 2 arguments.");
550       }
551       return new LogDoubleFunction(subExpressions.get(0), subExpressions.get(1));
552     } 
553     if (AnalyticsParams.DATE_OPERATION_SET.contains(operation)||AnalyticsParams.STRING_OPERATION_SET.contains(operation)) {
554       return null;
555     }
556     throw new SolrException(ErrorCode.BAD_REQUEST,"The operation ["+expressionString+"] is not supported.");
557   }
558 
559   
560   /**
561    * Recursively parses and breaks down the expression string to build a date ValueSource.
562    * 
563    * @param schema The schema to pull fields from.
564    * @param expressionString The expression string to build a ValueSource from.
565    * @return The value source represented by the given expressionString
566    */
567   @SuppressWarnings("deprecation")
568   private static ValueSource buildDateSource(IndexSchema schema, String expressionString) {
569     int paren = expressionString.indexOf('(');
570     String[] arguments;
571     if (paren<0) {
572       return buildFieldSource(schema, expressionString, DATE_TYPE);
573     } else {
574       arguments = ExpressionFactory.getArguments(expressionString.substring(paren+1, expressionString.lastIndexOf(')')).trim());
575     }
576     String operands = arguments[0];
577     String operation = expressionString.substring(0, paren).trim();
578     if (operation.equals(AnalyticsParams.CONSTANT_DATE)) {
579       if (arguments.length!=1) {
580         throw new SolrException(ErrorCode.BAD_REQUEST,"The constant date declaration ["+expressionString+"] does not have exactly 1 argument.");
581       }
582       try {
583         return new ConstDateSource(DateFormatUtil.parseDate(operands));
584       } catch (ParseException e) {
585         throw new SolrException(ErrorCode.BAD_REQUEST,"The constant "+operands+" cannot be converted into a date.",e);
586       }
587     } else if (operation.equals(AnalyticsParams.FILTER)) {
588       return buildFilterSource(schema, operands, DATE_TYPE);
589     }
590     if (operation.equals(AnalyticsParams.DATE_MATH)) {
591       List<ValueSource> subExpressions = new ArrayList<>();
592       boolean first = true;
593       for (String argument : arguments) {
594         ValueSource argSource;
595         if (first) {
596           first = false;
597           argSource = buildDateSource(schema, argument);
598           if (argSource == null) {
599             throw new SolrException(ErrorCode.BAD_REQUEST,"\""+AnalyticsParams.DATE_MATH+"\" requires the first argument be a date operation or field. ["+argument+"] is not a date operation or field.");
600           }
601         } else {
602           argSource = buildStringSource(schema, argument);
603           if (argSource == null) {
604             throw new SolrException(ErrorCode.BAD_REQUEST,"\""+AnalyticsParams.DATE_MATH+"\" requires that all arguments except the first be string operations. ["+argument+"] is not a string operation.");
605           }
606         }
607         subExpressions.add(argSource);
608       }
609       return new DateMathFunction(subExpressions.toArray(new ValueSource[0]));
610     }
611     if (AnalyticsParams.NUMERIC_OPERATION_SET.contains(operation)||AnalyticsParams.STRING_OPERATION_SET.contains(operation)) {
612       return null;
613     }
614     throw new SolrException(ErrorCode.BAD_REQUEST,"The operation ["+expressionString+"] is not supported.");
615   }
616 
617   
618   /**
619    * Recursively parses and breaks down the expression string to build a string ValueSource.
620    * 
621    * @param schema The schema to pull fields from.
622    * @param expressionString The expression string to build a ValueSource from.
623    * @return The value source represented by the given expressionString
624    */
625   private static ValueSource buildStringSource(IndexSchema schema, String expressionString) {
626     int paren = expressionString.indexOf('(');
627     String[] arguments;
628     if (paren<0) {
629       return buildFieldSource(schema, expressionString, FIELD_TYPE);
630     } else {
631       arguments = ExpressionFactory.getArguments(expressionString.substring(paren+1, expressionString.lastIndexOf(')')).trim());
632     }
633     String operands = arguments[0];
634     String operation = expressionString.substring(0, paren).trim();
635     if (operation.equals(AnalyticsParams.CONSTANT_STRING)) {
636       operands = expressionString.substring(paren+1, expressionString.lastIndexOf(')'));
637       return new ConstStringSource(operands);
638     } else if (operation.equals(AnalyticsParams.FILTER)) {
639       return buildFilterSource(schema,operands,FIELD_TYPE);
640     } else if (operation.equals(AnalyticsParams.REVERSE)) {
641       if (arguments.length!=1) {
642         throw new SolrException(ErrorCode.BAD_REQUEST,"\""+AnalyticsParams.REVERSE+"\" requires exactly one argument. The number of arguments in "+expressionString+" is not 1.");
643       }
644       return new ReverseStringFunction(buildStringSource(schema, operands));
645     }
646     List<ValueSource> subExpressions = new ArrayList<>();
647     for (String argument : arguments) {
648       subExpressions.add(buildSourceTree(schema, argument));
649     }
650     if (operation.equals(AnalyticsParams.CONCATENATE)) {
651       return new ConcatStringFunction(subExpressions.toArray(new ValueSource[0]));
652     } 
653     if (AnalyticsParams.NUMERIC_OPERATION_SET.contains(operation)) {
654       return buildNumericSource(schema, expressionString);
655     } else if (AnalyticsParams.DATE_OPERATION_SET.contains(operation)) {
656       return buildDateSource(schema, expressionString);
657     }
658     throw new SolrException(ErrorCode.BAD_REQUEST,"The operation ["+expressionString+"] is not supported.");
659   }
660 }